/*
 * Project: Sudoku Explainer
 * Copyright (C) 2006-2007 Nicolas Juillerat
 * 
 * Project: Sudoku Hint Engine for Android
 * Copyright (C) 2010-2013 Emanuel Moecklin
 * 
 * Available under the terms of the Lesser General Public License (LGPL)
 */
package com.onegravity.sudoku.solver.rules;

import java.util.BitSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import android.net.Uri;

import com.onegravity.sudoku.solver.HintResource;
import com.onegravity.sudoku.solver.HintType;
import com.onegravity.sudoku.solver.R;
import com.onegravity.sudoku.solver.core.IndirectHint;
import com.onegravity.sudoku.solver.core.Rule;
import com.onegravity.sudoku.solver.data.Cell;
import com.onegravity.sudoku.solver.data.ImmutableCell;
import com.onegravity.sudoku.solver.util.HtmlLoader;
import com.onegravity.sudoku.solver.util.ValuesFormatter;

public class AlignedSubsetExclusionHint extends IndirectHint implements Rule {

	private static final long serialVersionUID = -1902759468068179952L;
	
	private final Cell[] cells;
    private final Map<int[], Cell> lockedCombinations;

	private final boolean hasExtraRegions;
	private final boolean isJigsaw;

	public AlignedSubsetExclusionHint(Map<Cell, BitSet> removables, Cell[] cells, Map<int[], Cell> lockedCombinations, boolean hasExtraRegions, boolean isJigsaw) {
        super(removables);
        this.cells = cells;
        this.lockedCombinations = lockedCombinations;
        this.hasExtraRegions = hasExtraRegions;
        this.isJigsaw = isJigsaw;
    }

    @Override
    protected Cell[] getSelectedCells() {
        return cells;
    }

    @Override
    public Map<Cell, BitSet> getSecondarySelectedPotentials(HintType type, int viewNum) {
        BitSet releventValues = getRelevantCombinationValues();
        Map<Cell, BitSet> result = new HashMap<Cell, BitSet>();
        for (Cell cell : lockedCombinations.values()) {
            if (cell != null) {
                BitSet values = (BitSet)cell.getPotentialValues().clone();
                if (contains(releventValues, values)) result.put(cell, values);
            }
        }
        return (type!=HintType.SMALL_CLUE) ? result : null;
    }

    @Override
    public Map<ImmutableCell, BitSet> getRemovedPotentials(HintType type) {
        return (type==HintType.HINT) ? super.getRemovedPotentials(type) : null;
    }

    private BitSet getRelevantCombinationValues() {
        BitSet result = new BitSet(10);
        for (int[] combination : lockedCombinations.keySet()) {
            if (isRelevent(combination)) {
                for (int i = 0; i < combination.length; i++)
                    result.set(combination[i]);
            }
        }
        return result;
    }

    /**
     * Check whether all the bit of the subset <tt>subSet</tt>
     * are contained in the set <tt>set</tt>
     * @param set the set
     * @param subSet the subset
     * @return if the bits of <tt>set</tt> cover the bits of <tt>subSet</tt>
     */
    private boolean contains(BitSet set, BitSet subSet) {
        BitSet temp = (BitSet)set.clone();
        temp.or(subSet);
        return temp.cardinality() == set.cardinality();
    }

    @Override
    public String getName() {
        int degree = cells.length;
        return (degree == 2) ? HintResource.ALIGNED_PAIR_EXCLUSION.getName(HintResource.getString(R.string.hint_pair)) :
        	   (degree == 3) ? HintResource.ALIGNED_SUBSET_EXCLUSION.getName(HintResource.getString(R.string.hint_triple)) :
        	   (degree == 4) ? HintResource.ALIGNED_SUBSET_EXCLUSION.getName(HintResource.getString(R.string.hint_quad)) :
        	   HintResource.ALIGNED_SUBSET_EXCLUSION.getName(degree);
    }

    @Override
    public Uri getLink() {
    	return HintResource.ALIGNED_PAIR_EXCLUSION.getUrl();
	}

    @Override
    public String toString() {
        return getName() + ": " + Cell.toString(cells); //$NON-NLS-1$
    }

    @Override
    public double getDifficulty() {
        int degree = cells.length;
        return ((degree==2) ? 6.2 : (degree==3) ? 7.5 : 10.0) + (hasExtraRegions ? 0.2 : 0.0) + (isJigsaw ? 0.2 : 0.0);
    }

    @Override
    public String getHintHtml(HintType type) {
        int degree = cells.length;
    	HintResource hintRes = (degree==2) ? HintResource.ALIGNED_PAIR_EXCLUSION : HintResource.ALIGNED_SUBSET_EXCLUSION;
        String result = HtmlLoader.loadHtml(hintRes.hint(type));
        int[] namesRes = {R.string.hint_pair, R.string.hint_triple, R.string.hint_quad, R.string.hint_set5, R.string.hint_set6, R.string.hint_set7};
        String name = HintResource.getString(namesRes[degree - 2]);
        String andString = HintResource.getString(R.string.and);
        String cellNames = ValuesFormatter.formatCells(cells, " " + andString + " "); //$NON-NLS-1$ //$NON-NLS-2$

        String ruleList = ""; //$NON-NLS-1$
        if (type==HintType.HINT) {
        	// generate list of combinations to eliminate
	        StringBuilder rules = new StringBuilder();
	        rules.append("<ul>"); //$NON-NLS-1$
	        for (int[] combination : lockedCombinations.keySet()) {
	            Cell lockCell = lockedCombinations.get(combination);
	            if (isRelevent(combination)) {
	                appendCombination(rules, combination, lockCell);
	            }
	        }
	        rules.append("</ul>"); //$NON-NLS-1$
	        ruleList = rules.toString();
        }
        
        Cell[] exclCells = new Cell[super.getRemovablePotentials().size()];
        super.getRemovablePotentials().keySet().toArray(exclCells);
        String exclCellNames = ValuesFormatter.formatCells(exclCells, " " + andString + " "); //$NON-NLS-1$ //$NON-NLS-2$
        String exclValues = ValuesFormatter.formatValues(getRemovableValues(), ", "); //$NON-NLS-1$
        String plural_noun = (exclCells.length==1) ? "" : "s"; //$NON-NLS-1$ //$NON-NLS-2$
        String plural_verb = (exclCells.length==1) ? "s" : ""; //$NON-NLS-1$ //$NON-NLS-2$
        return HtmlLoader.format(result, name, cellNames, ruleList, exclCellNames, exclValues, plural_noun, plural_verb);
    }

    /**
     * Test if the given combination of values for the cells
     * are relevent for this rule.
     * A combination is relevent if it includes one of the
     * removable potential.
     * @param combination the combination of values
     * @return whether this combination is relevent
     */
    private boolean isRelevent(int[] combination) {
        Map<Cell, BitSet> removables = super.getRemovablePotentials();
        for (int i = 0; i < combination.length; i++) {
            Cell cell = cells[i];
            int value = combination[i];
            if (removables.containsKey(cell) && removables.get(cell).get(value))
                return true;
        }
        return false;
    }

    private BitSet getRemovableValues() {
        BitSet result = new BitSet(10);
        for (BitSet values : getRemovablePotentials().values()) {
            result.or(values);
        }
        return result;
    }

    private void appendCombination(StringBuilder builder, int[] combination, Cell lockCell) {
        builder.append("<li>"); //$NON-NLS-1$
        for (int i = 0; i < combination.length; i++) {
            String andString = HintResource.getString(R.string.and);
            if (i==combination.length-1) builder.append(" " + andString + " "); //$NON-NLS-1$ //$NON-NLS-2$
            else if (i>0) builder.append(", "); //$NON-NLS-1$
            builder.append("<b>"); //$NON-NLS-1$
            builder.append(Integer.toString(combination[i]));
            builder.append("</b>"); //$NON-NLS-1$
        }
        String reasoning = (lockCell==null) ?
						   HintResource.getString(R.string.hint_ase_reasoning1) :
						   HintResource.getString(R.string.hint_ase_reasoning2, lockCell.toString());
		builder.append(" " + reasoning); //$NON-NLS-1$
        builder.append("</li>"); //$NON-NLS-1$
    }

    private Set<Cell> cellSet() {
        Set<Cell> result = new HashSet<Cell>();
        for (Cell cell : cells) {
            result.add(cell);
        }
        return result;
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof AlignedSubsetExclusionHint)) return false;
        AlignedSubsetExclusionHint other = (AlignedSubsetExclusionHint)o;
        if (!this.cellSet().equals(other.cellSet())) return false;
        return this.getRemovablePotentials().equals(other.getRemovablePotentials());
    }

    @Override
    public int hashCode() {
        int result = getRemovablePotentials().hashCode();
        for (int i = 0; i < cells.length; i++)
            result = result ^ cells[i].hashCode();
        return result;
    }

}
